﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.CodeDom;
using System.Reflection;

namespace CoderJoe
{
    public class InstanceBuilder<TInterface>
    {
        public CodeTypeDeclaration Declaration { get; private set; }

        public void GenerateCode()
        {
            Type interfaceType = typeof(TInterface);
            Declaration = new CodeTypeDeclaration(interfaceType.Name + "AspectImpl");
            Declaration.BaseTypes.Add(interfaceType);

            Declaration.Members.Add(new CodeMemberField(typeof(AspectContainer), "Container"));
            Declaration.Members.Add(new CodeMemberField(interfaceType, "Instance"));
           

            CodeTypeConstructor constructor = new CodeTypeConstructor();
            constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(AspectContainer), "container"));
            constructor.Parameters.Add(new CodeParameterDeclarationExpression(interfaceType, "instance"));

            constructor.Statements.Add(
                new CodeAssignStatement(
                    new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "Container"),
                    new CodeArgumentReferenceExpression("container")));

            constructor.Statements.Add(
                new CodeAssignStatement(
                    new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "Instance"),
                    new CodeArgumentReferenceExpression("instance")));

            foreach (PropertyInfo property in interfaceType.GetProperties())
            {
                Declaration.Members.Add(GeneratePropertyImplementation(property));
            }

            var methods =
                (from m in interfaceType.GetMethods()
                 where m.IsSpecialName == false
                 select m);

            foreach (MethodInfo method in methods)
            {
                Declaration.Members.Add(GenerateMethodImplementation(method));
            }
        }

        private CodeTypeMember GenerateMethodImplementation(MethodInfo baseMethod)
        {
            CodeMemberMethod method = new CodeMemberMethod();
            ParameterInfo[] parameters = baseMethod.GetParameters();
            string delegateClassName = typeof(TInterface).Name + baseMethod.Name + parameters.Length.ToString() + "Invoker";
            CodeTypeDeclaration invoker = new CodeTypeDeclaration(delegateClassName);
            bool isFunction = (baseMethod.ReturnType != typeof(void));

            if (isFunction)
            {
                method.ReturnType = new CodeTypeReference(baseMethod.ReturnType);
            }

            Declaration.Members.Add(invoker);

            method.Name = baseMethod.Name;
            method.Attributes = MemberAttributes.Public;
                
            foreach (ParameterInfo parameterInfo in baseMethod.GetParameters())
            {
                CodeParameterDeclarationExpression parameter = 
                    new CodeParameterDeclarationExpression(
                        parameterInfo.ParameterType, parameterInfo.Name);

                method.Parameters.Add(parameter);

                CodeMemberField field = new CodeMemberField(parameterInfo.ParameterType, parameterInfo.Name);
                field.Attributes = MemberAttributes.Public;
                invoker.Members.Add(field);
            }

            if (isFunction)
            {
                CodeMemberField field = new CodeMemberField(baseMethod.ReturnType, "result");
                field.Attributes = MemberAttributes.Public;
                invoker.Members.Add(field);
            }

            CodeMemberField invokerInstanceField = new CodeMemberField(typeof(TInterface), "instance");
            invokerInstanceField.Attributes = MemberAttributes.Public;
            invoker.Members.Add(invokerInstanceField);

            CodeMemberMethod invokerMethod = new CodeMemberMethod();
            invokerMethod.Name = "Invoke";
            invokerMethod.Attributes = MemberAttributes.Public;

            CodeMethodInvokeExpression baseInvoke = new CodeMethodInvokeExpression(
                new CodeMethodReferenceExpression(
                    new CodeFieldReferenceExpression(
                        new CodeThisReferenceExpression(),
                        "instance"), baseMethod.Name),
                        (from p in  parameters
                         select new CodeFieldReferenceExpression(
                             new CodeThisReferenceExpression(), p.Name)).ToArray());

            if (isFunction)
            {
                invokerMethod.Statements.Add(
                    new CodeAssignStatement(
                        new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "result"),
                        baseInvoke));
            }
            else
            {
                invokerMethod.Statements.Add(baseInvoke);
            }

            invoker.Members.Add(invokerMethod);

            method.Statements.Add(new CodeVariableDeclarationStatement(
                new CodeTypeReference(delegateClassName), "invoker",
                new CodeObjectCreateExpression(
                    new CodeTypeReference(delegateClassName))));

            method.Statements.Add(
            new CodeAssignStatement(
                new CodeFieldReferenceExpression(
                    new CodeVariableReferenceExpression("invoker"), "instance"),
                    new CodeFieldReferenceExpression(
                        new CodeThisReferenceExpression(),
                        "Instance")));

            foreach (ParameterInfo parameterInfo in parameters)
            {
                method.Statements.Add(
                    new CodeAssignStatement(
                        new CodeFieldReferenceExpression(
                            new CodeVariableReferenceExpression("invoker"), parameterInfo.Name),
                            new CodeArgumentReferenceExpression(parameterInfo.Name)));
            }

            method.Statements.Add(
                new CodeMethodInvokeExpression(
                    new CodeMethodReferenceExpression(
                        new CodeVariableReferenceExpression("invoker"),
                        "Invoke")));

            method.Statements.Add(
                new CodeMethodInvokeExpression(
                    new CodeMethodReferenceExpression(
                        new CodeFieldReferenceExpression(
                            new CodeThisReferenceExpression(), "Container"), "Invoke"),
                            new CodeObjectCreateExpression(
                                new CodeTypeReference(typeof(Action)),
                                new CodeMethodReferenceExpression(
                                    new CodeVariableReferenceExpression("invoker"), "Invoke"))));

            if (isFunction)
            {
                method.Statements.Add(
                    new CodeMethodReturnStatement(
                        new CodeFieldReferenceExpression(
                            new CodeVariableReferenceExpression("invoker"), "result")));
            }

            return method;
        }

        private CodeTypeMember GeneratePropertyImplementation(PropertyInfo baseProperty)
        {
            CodeMemberProperty property = new CodeMemberProperty();
            property.Attributes = MemberAttributes.Public;
            property.Name = baseProperty.Name;

            property.HasGet = baseProperty.CanRead;
            property.HasSet = baseProperty.CanWrite;
            property.Type = new CodeTypeReference(baseProperty.PropertyType);

            if (baseProperty.CanRead)
            {
                property.GetStatements.Add(
                    new CodeMethodReturnStatement(
                        new CodePropertyReferenceExpression(
                        new CodeFieldReferenceExpression(
                            new CodeThisReferenceExpression(), "Instance"), baseProperty.Name)));
            }

            if (baseProperty.CanWrite)
            {
                property.SetStatements.Add(
                    new CodeAssignStatement(
                        new CodePropertyReferenceExpression(
                            new CodeFieldReferenceExpression(
                                new CodeThisReferenceExpression(), "Instance"), baseProperty.Name),
                        new CodePropertySetValueReferenceExpression()));
            }

            return property;
        }
    }
}
